/*
 * Copyright (C) 2019 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.server.compat;

import static android.app.compat.PackageOverride.VALUE_DISABLED;
import static android.app.compat.PackageOverride.VALUE_ENABLED;
import static android.app.compat.PackageOverride.VALUE_UNDEFINED;

import android.annotation.Nullable;
import android.app.compat.PackageOverride;
import android.compat.annotation.ChangeId;
import android.compat.annotation.Disabled;
import android.compat.annotation.EnabledSince;
import android.compat.annotation.Overridable;
import android.content.pm.ApplicationInfo;

import com.android.internal.compat.AndroidBuildClassifier;
import com.android.internal.compat.CompatibilityChangeInfo;
import com.android.internal.compat.OverrideAllowedState;
import com.android.server.compat.config.Change;
import com.android.server.compat.overrides.ChangeOverrides;
import com.android.server.compat.overrides.OverrideValue;
import com.android.server.compat.overrides.RawOverrideValue;

import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * Represents the state of a single compatibility change.
 *
 * <p>A compatibility change has a default setting, determined by the {@code enableAfterTargetSdk}
 * and {@code disabled} constructor parameters. If a change is {@code disabled}, this overrides any
 * target SDK criteria set. These settings can be overridden for a specific package using
 * {@link #addPackageOverrideInternal(String, boolean)}.
 *
 * <p>Note, this class is not thread safe so callers must ensure thread safety.
 */
public final class CompatChange extends CompatibilityChangeInfo {

    /**
     * A change ID to be used only in the CTS test for this SystemApi
     */
    @ChangeId
    @EnabledSince(targetSdkVersion = 31) // Needs to be > test APK targetSdkVersion.
    static final long CTS_SYSTEM_API_CHANGEID = 149391281; // This is a bug id.

    /**
     * An overridable change ID to be used only in the CTS test for this SystemApi
     */
    @ChangeId
    @Disabled
    @Overridable
    static final long CTS_SYSTEM_API_OVERRIDABLE_CHANGEID = 174043039; // This is a bug id.


    /**
     * Callback listener for when compat changes are updated for a package.
     * See {@link #registerListener(ChangeListener)} for more details.
     */
    public interface ChangeListener {
        /**
         * Called upon an override change for packageName and the change this listener is
         * registered for. Called before the app is killed.
         */
        void onCompatChange(String packageName);
    }

    ChangeListener mListener = null;

    private ConcurrentHashMap<String, Boolean> mEvaluatedOverrides;
    private ConcurrentHashMap<String, PackageOverride> mRawOverrides;

    public CompatChange(long changeId) {
        this(changeId, null, -1, -1, false, false, null, false);
    }

    /**
     * @param change an object generated by services/core/xsd/platform-compat-config.xsd
     */
    public CompatChange(Change change) {
        this(change.getId(), change.getName(), change.getEnableAfterTargetSdk(),
                change.getEnableSinceTargetSdk(), change.getDisabled(), change.getLoggingOnly(),
                change.getDescription(), change.getOverridable());
    }

    /**
     * @param changeId Unique ID for the change. See {@link android.compat.Compatibility}.
     * @param name Short descriptive name.
     * @param enableAfterTargetSdk {@code targetSdkVersion} restriction. See {@link EnabledAfter};
     *                             -1 if the change is always enabled.
     * @param enableSinceTargetSdk {@code targetSdkVersion} restriction. See {@link EnabledSince};
     *                             -1 if the change is always enabled.
     * @param disabled If {@code true}, overrides any {@code enableAfterTargetSdk} set.
     */
    public CompatChange(long changeId, @Nullable String name, int enableAfterTargetSdk,
            int enableSinceTargetSdk, boolean disabled, boolean loggingOnly, String description,
            boolean overridable) {
        super(changeId, name, enableAfterTargetSdk, enableSinceTargetSdk, disabled, loggingOnly,
              description, overridable);

        // Initialize override maps.
        mEvaluatedOverrides = new ConcurrentHashMap<>();
        mRawOverrides = new ConcurrentHashMap<>();
    }

    synchronized void registerListener(ChangeListener listener) {
        if (mListener != null) {
            throw new IllegalStateException(
                    "Listener for change " + toString() + " already registered.");
        }
        mListener = listener;
    }


    /**
     * Force the enabled state of this change for a given package name. The change will only take
     * effect after that packages process is killed and restarted.
     *
     * @param pname Package name to enable the change for.
     * @param enabled Whether or not to enable the change.
     */
    private void addPackageOverrideInternal(String pname, boolean enabled) {
        if (getLoggingOnly()) {
            throw new IllegalArgumentException(
                    "Can't add overrides for a logging only change " + toString());
        }
        mEvaluatedOverrides.put(pname, enabled);
        notifyListener(pname);
    }

    private void removePackageOverrideInternal(String pname) {
        if (mEvaluatedOverrides.remove(pname) != null) {
            notifyListener(pname);
        }
    }

    /**
     * Tentatively set the state of this change for a given package name.
     * The override will only take effect after that package is installed, if applicable.
     *
     * @param packageName Package name to tentatively enable the change for.
     * @param override The package override to be set
     * @param allowedState Whether the override is allowed.
     * @param versionCode The version code of the package.
     */
    synchronized void addPackageOverride(String packageName, PackageOverride override,
            OverrideAllowedState allowedState, @Nullable Long versionCode) {
        if (getLoggingOnly()) {
            throw new IllegalArgumentException(
                    "Can't add overrides for a logging only change " + toString());
        }
        mRawOverrides.put(packageName, override);
        recheckOverride(packageName, allowedState, versionCode);
    }

    /**
     * Rechecks an existing (and possibly deferred) override.
     *
     * <p>For deferred overrides, check if they can be promoted to a regular override. For regular
     * overrides, check if they need to be demoted to deferred.</p>
     *
     * @param packageName Package name to apply deferred overrides for.
     * @param allowedState Whether the override is allowed.
     * @param versionCode The version code of the package.
     *
     * @return {@code true} if the recheck yielded a result that requires invalidating caches
     *         (a deferred override was consolidated or a regular override was removed).
     */
    synchronized boolean recheckOverride(String packageName, OverrideAllowedState allowedState,
            @Nullable Long versionCode) {
        if (packageName == null) {
            return false;
        }
        boolean allowed = (allowedState.state == OverrideAllowedState.ALLOWED);
        // If the app is not installed or no longer has raw overrides, evaluate to false
        if (versionCode == null || !mRawOverrides.containsKey(packageName) || !allowed) {
            removePackageOverrideInternal(packageName);
            return false;
        }
        // Evaluate the override based on its version
        int overrideValue = mRawOverrides.get(packageName).evaluate(versionCode);
        switch (overrideValue) {
            case VALUE_UNDEFINED:
                removePackageOverrideInternal(packageName);
                break;
            case VALUE_ENABLED:
                addPackageOverrideInternal(packageName, true);
                break;
            case VALUE_DISABLED:
                addPackageOverrideInternal(packageName, false);
                break;
        }
        return true;
    }

    /**
     * Remove any package override for the given package name, restoring the default behaviour.
     *
     * <p>Note, this method is not thread safe so callers must ensure thread safety.
     *
     * @param pname Package name to reset to defaults for.
     * @param allowedState Whether the override is allowed.
     * @param versionCode The version code of the package.
     */
    synchronized boolean removePackageOverride(String pname, OverrideAllowedState allowedState,
            @Nullable Long versionCode) {
        if (mRawOverrides.containsKey(pname)) {
            allowedState.enforce(getId(), pname);
            mRawOverrides.remove(pname);
            recheckOverride(pname, allowedState, versionCode);
            return true;
        }
        return false;
    }

    /**
     * Find if this change is enabled for the given package, taking into account any overrides that
     * exist.
     *
     * @param app Info about the app in question
     * @return {@code true} if the change should be enabled for the package.
     */
    boolean isEnabled(ApplicationInfo app, AndroidBuildClassifier buildClassifier) {
        if (app == null) {
            return defaultValue();
        }
        if (app.packageName != null) {
            final Boolean enabled = mEvaluatedOverrides.get(app.packageName);
            if (enabled != null) {
                return enabled;
            }
        }
        if (getDisabled()) {
            return false;
        }
        if (getEnableSinceTargetSdk() != -1) {
            // If the change is gated by a platform version newer than the one currently installed
            // on the device, disregard the app's target sdk version.
            int compareSdk = Math.min(app.targetSdkVersion, buildClassifier.platformTargetSdk());
            return compareSdk >= getEnableSinceTargetSdk();
        }
        return true;
    }

    /**
     * Find if this change will be enabled for the given package after installation.
     *
     * @param packageName The package name in question
     * @return {@code true} if the change should be enabled for the package.
     */
    boolean willBeEnabled(String packageName) {
        if (packageName == null) {
            return defaultValue();
        }
        final PackageOverride override = mRawOverrides.get(packageName);
        if (override != null) {
            switch (override.evaluateForAllVersions()) {
                case VALUE_ENABLED:
                    return true;
                case VALUE_DISABLED:
                    return false;
                case VALUE_UNDEFINED:
                    return defaultValue();
            }
        }
        return defaultValue();
    }

    /**
     * Returns the default value for the change id, assuming there are no overrides.
     *
     * @return {@code false} if it's a default disabled change, {@code true} otherwise.
     */
    boolean defaultValue() {
        return !getDisabled();
    }

    synchronized void clearOverrides() {
        mRawOverrides.clear();
        mEvaluatedOverrides.clear();
    }

    synchronized void loadOverrides(ChangeOverrides changeOverrides) {
        // Load deferred overrides for backwards compatibility
        if (changeOverrides.getDeferred() != null) {
            for (OverrideValue override : changeOverrides.getDeferred().getOverrideValue()) {
                mRawOverrides.put(override.getPackageName(),
                        new PackageOverride.Builder().setEnabled(
                                override.getEnabled()).build());
            }
        }

        // Load validated overrides. For backwards compatibility, we also add them to raw overrides.
        if (changeOverrides.getValidated() != null) {
            for (OverrideValue override : changeOverrides.getValidated().getOverrideValue()) {
                mEvaluatedOverrides.put(override.getPackageName(), override.getEnabled());
                mRawOverrides.put(override.getPackageName(),
                        new PackageOverride.Builder().setEnabled(
                                override.getEnabled()).build());
            }
        }

        // Load raw overrides
        if (changeOverrides.getRaw() != null) {
            for (RawOverrideValue override : changeOverrides.getRaw().getRawOverrideValue()) {
                PackageOverride packageOverride = new PackageOverride.Builder()
                        .setMinVersionCode(override.getMinVersionCode())
                        .setMaxVersionCode(override.getMaxVersionCode())
                        .setEnabled(override.getEnabled())
                        .build();
                mRawOverrides.put(override.getPackageName(), packageOverride);
            }
        }
    }

    synchronized ChangeOverrides saveOverrides() {
        if (mRawOverrides.isEmpty()) {
            return null;
        }
        ChangeOverrides changeOverrides = new ChangeOverrides();
        changeOverrides.setChangeId(getId());
        ChangeOverrides.Raw rawOverrides = new ChangeOverrides.Raw();
        List<RawOverrideValue> rawList = rawOverrides.getRawOverrideValue();
        for (Map.Entry<String, PackageOverride> entry : mRawOverrides.entrySet()) {
            RawOverrideValue override = new RawOverrideValue();
            override.setPackageName(entry.getKey());
            override.setMinVersionCode(entry.getValue().getMinVersionCode());
            override.setMaxVersionCode(entry.getValue().getMaxVersionCode());
            override.setEnabled(entry.getValue().isEnabled());
            rawList.add(override);
        }
        changeOverrides.setRaw(rawOverrides);

        ChangeOverrides.Validated validatedOverrides = new ChangeOverrides.Validated();
        List<OverrideValue> validatedList = validatedOverrides.getOverrideValue();
        for (Map.Entry<String, Boolean> entry : mEvaluatedOverrides.entrySet()) {
            OverrideValue override = new OverrideValue();
            override.setPackageName(entry.getKey());
            override.setEnabled(entry.getValue());
            validatedList.add(override);
        }
        changeOverrides.setValidated(validatedOverrides);
        return changeOverrides;
    }

    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder("ChangeId(")
                .append(getId());
        if (getName() != null) {
            sb.append("; name=").append(getName());
        }
        if (getEnableSinceTargetSdk() != -1) {
            sb.append("; enableSinceTargetSdk=").append(getEnableSinceTargetSdk());
        }
        if (getDisabled()) {
            sb.append("; disabled");
        }
        if (getLoggingOnly()) {
            sb.append("; loggingOnly");
        }
        if (!mEvaluatedOverrides.isEmpty()) {
            sb.append("; packageOverrides=").append(mEvaluatedOverrides);
        }
        if (!mRawOverrides.isEmpty()) {
            sb.append("; rawOverrides=").append(mRawOverrides);
        }
        if (getOverridable()) {
            sb.append("; overridable");
        }
        return sb.append(")").toString();
    }

    private synchronized void notifyListener(String packageName) {
        if (mListener != null) {
            mListener.onCompatChange(packageName);
        }
    }
}
